package com.erju.filter;

import com.erju.utils.JWTToken;
import com.erju.utils.JWTUtil;
import com.erju.utils.RedisUtil;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;

/**
 * @Description: 来辅助shiro进行过滤处理。所有的请求都会到过滤器来进行处理
 * @author: 86132
 * @date: 2022.03.07
 */
public class JWTFilter extends BasicHttpAuthenticationFilter {
    private static final Logger log = LoggerFactory.getLogger(JWTFilter.class);


    /**
     * 是否允许访问，如果带有 token，则对 token 进行检查，否则直接通过
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        log.info("Tonken校验");
        //判断请求的请求头是否带上 "Token"
        if (isLoginAttempt(request, response)) {
            //如果存在，则进入 executeLogin 方法执行登入，检查 token 是否正确
            try {
                //去检查token
                if (executeLogin(request, response)) {
                    return true;
                } else {
                    return false;
                }
            } catch (Exception e) {
                //token 错误
                responseError(response, e.getMessage());
            }
        }
        //如果请求头不存在 Token，则可能是执行登陆操作或者是游客状态访问，无需检查 token，直接返回 true
        return false;
    }

    /**
     * 判断用户是否想要登入。
     * 没有token return false
     * 有token return true
     * 检测 header 里面是否包含 Token 字段
     */

    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        System.out.println("isLoginAttempt");
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader("Authorization");
        log.info("isLoginAttempt -> 请求头中的token" + token);
        System.out.println(token);
        //没有token return false | 有token return true 然后去校验
        return token != null;
    }

    /**
     * executeLogin
     * 实际上就是先调用createToken来获取token，这里我们重写了这个方法，
     * 就不会自动去调用createToken来获取token
     * 然后调用getSubject方法来获取当前用户再调用login方法来实现登录
     * 这也解释了我们为什么要自定义jwtToken，因为我们不再使用Shiro默认的UsernamePasswordToken了。
     *
     * @param request
     * @param response
     * @return boolean  -> true
     * @throws Exception
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        log.info("executeLogin --- ");
        HttpServletRequest req = (HttpServletRequest) request;
        //从请求头获取token
        RedisUtil redisUtil = getBean(RedisUtil.class, req);
        String token = req.getHeader("Authorization");
        //看redis中是否有请求头带过来的token
        if (redisUtil.hasKey(token)) {
            log.info("executeLogin请求头中的token" + token);
            //看token是否是自己的
            String userNameFromToken = JWTUtil.getUsername(token);
            String userNameFromRedis = (String) redisUtil.get(token);
            log.info("userNameFromRedis --> redis中获取到的 userNameFromRedis" + userNameFromRedis);
            if (userNameFromToken.equals(userNameFromRedis)) {
                //有token且校验合法刷新token过期时间
                redisUtil.set(token, userNameFromRedis, 60 * 60 * 24);
                JWTToken jwt = new JWTToken(token);
                //交给自定义的realm对象去登录，如果错误他会抛出异常并被捕获
                getSubject(request, response).login(jwt);
                //全部检验合法
                return true;
            } else {
                //有token 但是用户名不匹配
                return false;
            }
        } else {
            //没有token或不合法--> 过期或者被修改,抛出异常信息，跳转到登陆页面
            return false;
        }

    }

    /**
     * 这里的getBean是因为使用@Autowired无法把RedisUtil注入进来
     * 这样自动去注入当使用的时候是未NULL，是注入不进去了。通俗的来讲是因为拦截器在spring扫描bean之前加载所以注入不进去。
     * <p>
     * 解决的方法：
     * 可以通过已经初始化之后applicationContext容器中去获取需要的bean.
     *
     * @param clazz
     * @param request
     * @param <T>
     * @return
     */
    public <T> T getBean(Class<T> clazz, HttpServletRequest request) {
        WebApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
        return applicationContext.getBean(clazz);
    }


    //    下面的不用管

    /**
     * 解决跨域
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
//    @Override
//    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
//        System.out.println("preHandle");
//        HttpServletRequest req = (HttpServletRequest) request;
//        HttpServletResponse res = (HttpServletResponse) response;
//        res.setHeader("Access-control-Allow-Origin", req.getHeader("Origin"));
//        res.setHeader("Access-control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
//        res.setHeader("Access-control-Allow-Headers", req.getHeader("Access-Control-Request-Headers"));
//        // 跨域时会首先发送一个option请求，这里我们给option请求直接返回正常状态
//        if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
//            res.setStatus(HttpStatus.OK.value());
//            return false;
//        }
//        return super.preHandle(request, response);
//    }

    /**
     * 将非法请求跳转到 http://localhost:8888/unauthorized
     */
    private void responseError(ServletResponse response, String message) {
        System.out.println("responseError");
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            //设置编码，否则中文字符在重定向时会变为空字符串
            message = URLEncoder.encode(message, "UTF-8");
            httpServletResponse.sendRedirect("http://localhost:8888/unauthorized");
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
}
